/**
 * Software License, Version 1.0
 * 
 * Copyright 2003 The Trustees of Indiana University. All rights reserved.
 * 
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 * 1) All redistributions of source code must retain the above copyright notice,
 * the list of authors in the original source code, this list of conditions and
 * the disclaimer listed in this license; 2) All redistributions in binary form
 * must reproduce the above copyright notice, this list of conditions and the
 * disclaimer listed in this license in the documentation and/or other materials
 * provided with the distribution; 3) Any documentation included with all
 * redistributions must include the following acknowledgement:
 * 
 * "This product includes software developed by the Community Grids Lab. For
 * further information contact the Community Grids Lab at
 * http://communitygrids.iu.edu/."
 * 
 * Alternatively, this acknowledgement may appear in the software itself, and
 * wherever such third-party acknowledgments normally appear.
 * 
 * 4) The name Indiana University or Community Grids Lab or NaradaBrokering,
 * shall not be used to endorse or promote products derived from this software
 * without prior written permission from Indiana University. For written
 * permission, please contact the Advanced Research and Technology Institute
 * ("ARTI") at 351 West 10th Street, Indianapolis, Indiana 46202. 5) Products
 * derived from this software may not be called NaradaBrokering, nor may Indiana
 * University or Community Grids Lab or NaradaBrokering appear in their name,
 * without prior written permission of ARTI.
 * 
 * 
 * Indiana University provides no reassurances that the source code provided
 * does not infringe the patent or any other intellectual property rights of any
 * other entity. Indiana University disclaims any liability to any recipient for
 * claims brought by any other entity based on infringement of intellectual
 * property rights or otherwise.
 * 
 * LICENSEE UNDERSTANDS THAT SOFTWARE IS PROVIDED "AS IS" FOR WHICH NO
 * WARRANTIES AS TO CAPABILITIES OR ACCURACY ARE MADE. INDIANA UNIVERSITY GIVES
 * NO WARRANTIES AND MAKES NO REPRESENTATION THAT SOFTWARE IS FREE OF
 * INFRINGEMENT OF THIRD PARTY PATENT, COPYRIGHT, OR OTHER PROPRIETARY RIGHTS.
 * INDIANA UNIVERSITY MAKES NO WARRANTIES THAT SOFTWARE IS FREE FROM "BUGS",
 * "VIRUSES", "TROJAN HORSES", "TRAP DOORS", "WORMS", OR OTHER HARMFUL CODE.
 * LICENSEE ASSUMES THE ENTIRE RISK AS TO THE PERFORMANCE OF SOFTWARE AND/OR
 * ASSOCIATED MATERIALS, AND TO THE PERFORMANCE AND VALIDITY OF INFORMATION
 * GENERATED USING SOFTWARE.
 */
package cgl.narada.transport.up2p;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Hashtable;
import java.util.Properties;
import java.util.StringTokenizer;

import org.apache.log4j.Logger;

import cgl.narada.protocol.Protocol;
import cgl.narada.transport.Link;
import cgl.narada.transport.LinkFactory;
import cgl.narada.transport.TransportDebugFlags;
import cgl.narada.transport.TransportException;
import cgl.narada.transport.udp.UDPLink;
import cgl.narada.transport.udp.UDPLinkFactory;
import cgl.narada.transport.udp.UDPReceiverThread;
import cgl.narada.transport.up2p.messages.P2PConnectionRequest;
import cgl.narada.transport.up2p.messages.P2PConnectionResponse;
import cgl.narada.transport.up2p.messages.P2PConnectionSetupRequest;
import cgl.narada.util.PortRetriever;
import cgl.narada.util.UUIDRetriever;

/**
 * This is a UDP based P2P connetion link implementation. It uses the UDP hole
 * punching technique while leveraging the existing UDP Link factory for
 * processing connections
 * 
 * <p>
 * To initialize the UP2PLinkFactory one needs to specify the following
 * <li><b>UP2PListenerPort </b>, this is the port on which the process running
 * this factory would listen to for datagram packet communications</li>
 * <li><b>PeerID</b>, a unique peer ID used by this peer</li>
 * <li><b>RelayServerHost</b>, the location of the relay server</li>
 * <li><b>RelayServerPort</b>, the relay server port</li>
 * <p>
 * 
 * To create a link to node B. The following properties needs to be specified.
 * <li><b>hostname</b> - The destination peer's PeerID to which a connection
 * needs to be established. The destination peer's host/port will be obtained
 * from the relay server
 * 
 * @author Harshawardhan Gadgil
 */

public class UP2PLinkFactory extends UDPLinkFactory implements LinkFactory,
         TransportDebugFlags {

   static Logger log = Logger.getLogger("UP2PLinkFactory");

   private String managedLinkType = "up2p";
   private Properties properties;
   private String moduleName = "UP2PLinkFactory: ";
   private int udpListenerPort = 3045;

   private Object waitForPeerDetails;
   private P2PConnectionResponse pd;
   private InetSocketAddress relayServerAddress;

   /** Contains a list of UDPLink Objects with magic as keys */
   private Hashtable magicTable;

   // Temp stuff for no UDP link coz' hashtable doesn't allow null values
   private Integer nullInteger = new Integer(0);
   
   private PeerRegistrationThread peerRegistrationThread;

   public UP2PLinkFactory() {
      super();
      waitForPeerDetails = new Object();
      magicTable = new Hashtable();
   }

   /*
    * These are the set of properties that are needed to set up the link
    * Factory.
    */
   public void setLinkFactoryProperties(Properties factoryProperties) {
      if (factoryProperties == null) {
         System.out.println(moduleName + "The specified properties is NULL");
         return;
      }
      properties = factoryProperties;
      String _valueString = properties.getProperty("UP2PListenerPort");
      if (_valueString != null) {
         udpListenerPort = Integer.parseInt(_valueString);
      } else {
         udpListenerPort = PortRetriever
                  .retrieveAvailableDatagramPort(udpListenerPort);
         System.out.println(moduleName + "Property UDPListenerPort missing, "
                  + " will use [" + udpListenerPort + "]");
      }

      // UP2P specific properties...

      _valueString = properties.getProperty("PeerID");
      if (_valueString == null) {
         _valueString = "peer";
         System.out.println(moduleName + "Property PeerID missing, "
                  + " will use [" + _valueString + "]");
      }
      properties.put("PeerID", _valueString);

      _valueString = properties.getProperty("RelayServerHost");
      if (_valueString == null) {
         _valueString = "trex.ucs.indiana.edu";
         System.out.println(moduleName + "Property RelayServerHost missing, "
                  + " will use [" + _valueString + "]");
      }
      properties.put("RelayServerHost", _valueString);

      _valueString = properties.getProperty("RelayServerPort");
      if (_valueString == null) {
         _valueString = "60055";
         System.out.println(moduleName + "Property RelayServerPort missing, "
                  + " will use [" + _valueString + "]");
      }
      properties.put("RelayServerPort", _valueString);
   }

   /** Return the type of all the links created/managed by this factory */
   public String getManagedLinksType() {
      return managedLinkType;
   }

   /**
    * Create a communication link based on the properties that have been
    * specified.
    */
   public Link createLink(Properties linkProperties) throws TransportException {
      if (linkProperties == null) { throw new TransportException(moduleName
               + "The specified link propertis is null"); }

      // This is modified request for P2P connection links...
      String destPeerID = linkProperties.getProperty("hostname");

      // This uuid is used for this particular connection and serves as the
      // magic for this connection ONLY
      String magic = UUIDRetriever.getInstance().getTimeBasedUUIDAsString();

      pd = null;

      // Create a temp entry in magic table
      magicTable.put(magic, nullInteger);
      System.out.println(">>>> MAGIC Created [" + magic + "]");

      // Sends a connection request to Relay server who will respond with the
      // destination peer's public IP:Port
      P2PConnectionRequest req = new P2PConnectionRequest(destPeerID, magic);

      byte[] bytes = req.getBytes();

      byte[] data = new byte[1 + bytes.length];
      data[0] = Protocol.P2P_CONN_REQUEST;
      System.arraycopy(bytes, 0, data, 1, bytes.length);

      try {
         DatagramPacket packet = new DatagramPacket(data, data.length,
                  relayServerAddress);
         log.debug("Sending P2PConnectionRequest !");
         udpListenerSocket.send(packet);
      } catch (Exception e1) {
         log.error("", e1);
      }

      // Wait up to 10 sec for response...
      synchronized (waitForPeerDetails) {
         try {
            waitForPeerDetails.wait(10000);
         } catch (InterruptedException e) {
            // IGNORE
         }
      }

      if (pd == null) {
         String errorInfo = moduleName + " Unable to Setup Connection to ["
                  + destPeerID + "]";
         System.out.println(errorInfo);
         throw new TransportException(errorInfo);
      }

      InetSocketAddress addr = (InetSocketAddress) pd.getPublicAddress();

      String _hostName = addr.getHostName(); // linkProperties.getProperty("hostname");
      String _portString = "" + addr.getPort(); // linkProperties.getProperty("portnum");

      if (_hostName == null || _portString == null) { throw new TransportException(
               moduleName + "One of the specified properties is null"); }

      int _portNum = Integer.parseInt(_portString);

      try {
         InetAddress _ia = InetAddress.getByName(_hostName);
         _hostName = _ia.getCanonicalHostName();

         String _key = "udp://" + _hostName + ":" + _portNum;

         if (links.containsKey(_key)) { throw new TransportException(moduleName
                  + "Link already exists: " + _key); }

         UDPLink _link = new UDPLink(udpListenerSocket, _ia, _portNum, this);

         _link.setLinkId(_key);

         // Registration should ONLY occur when a CONNECTION Response has been
         // obtained
         // 
         // transportHandler.registerLink(this, _link);

         /** Force the creation of a link on the other side too */
         byte[] magicBytes = pd.getMagic().getBytes();
         byte[] udpConnRequest = new byte[1 + magicBytes.length];
         udpConnRequest[0] = cgl.narada.protocol.Protocol.CONN_REQ;

         System.arraycopy(magicBytes, 0, udpConnRequest, 1, magicBytes.length);

         // Try 5 times with 1 sec interval... OR until a link with our current
         // magic is established...
         for (int z = 0; z < 5; z++) {
            if (linkEstablished(magic)) {
               System.out.println("*** Link for MAGIC <" + magic
                        + "> ESTABLISHED !");
               break;
            }
            System.out.println("Trying {" + _link.getLinkId() + "}: " + (z + 1)
                     + " times");
            _link.sendData(udpConnRequest);
            try {
               Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
         }

         if (linkEstablished(magic)) {
            links.put(_key, _link);
            // magicTable.put(magic, _link);

            _link = (UDPLink) magicTable.get(magic);
            System.out.println("*** ESTABLISHED LINK_ID: " + _link.getLinkId());
            return _link;
         }

         String errorInfo = moduleName + " Unable to Setup Connection to ["
                  + _hostName + ":" + _portNum + "]";
         System.out.println(errorInfo);
         throw new TransportException(errorInfo);

      } catch (UnknownHostException uhe) {
         String errorInfo = moduleName + "Unknown host [" + _hostName + "]"
                  + uhe;
         System.out.println(errorInfo);
         throw new TransportException(errorInfo);
      } catch (IOException ioe) {
         String errorInfo = moduleName + " Unable to Setup Connection to ["
                  + _hostName + ":" + _portNum + "] => " + ioe;
         System.out.println(errorInfo);
         throw new TransportException(errorInfo);
      }
   }
   
   /**
    * This method is used to garbage collect any resources associated with the
    * link factory
    */
   public void dispose() {
     super.dispose();
     peerRegistrationThread.shutdown();
   }

   private boolean linkEstablished(String magic) {

      Object o = magicTable.get(magic);
      if (o == null) return false;
      if (o.equals(nullInteger)) return false;

      return true;
   }

   /**
    * Starts communication services. In this case what we do is start the
    * UDPListener thread, which listens to datagram packets arriving at the
    * UDPListenerPort
    */
   public boolean loadLinkFactoryServices() throws TransportException {
      try {
         udpListenerSocket = new DatagramSocket(udpListenerPort);
      } catch (SocketException se) {
         throw new TransportException(moduleName + se);
      }
      udpReceiverThread = new UDPReceiverThread(udpListenerSocket, this,
               udppinghandler);
      udpReceiverThread.start();

      // Also start the Peer registration thread...

      String peerID = properties.getProperty("PeerID");
      String relayServerHost = properties.getProperty("RelayServerHost");
      String relayServerPort = properties.getProperty("RelayServerPort");

      relayServerAddress = new InetSocketAddress(relayServerHost, Integer
               .parseInt(relayServerPort));

      if (peerID == null || relayServerHost == null || relayServerPort == null) {
         // Cleanup and return false
         udpReceiverThread.stop();
         udpListenerSocket.close();
         return false;
      }

       peerRegistrationThread = new PeerRegistrationThread(
               peerID, udpListenerSocket, relayServerAddress);
      peerRegistrationThread.start();

      System.out.println("Loaded UP2PLinkFactory communication services");
      return true;
   }

   /**
    * This handles the connection request from the destination peer. The
    * connection is correlated with the magic bundled with the connection
    * request.
    * 
    * @param remoteAddress
    * @param remotePort
    * @param magic
    */
   public void handleP2P_CONN_REQ(InetAddress remoteAddress, int remotePort,
            String magic) {

      String _linkId = "udp://" + remoteAddress.getHostName() + ":"
               + remotePort;

      // If this is already done once, then don't bother doing it again...
      if (linkEstablished(magic)) return;

      log.info("Connection rcvd for <" + magic + ">");

      if (UDPLinkFactory_Debug) {
         System.out.println(moduleName + "InboundConnection request received "
                  + "from [" + _linkId + "]");
      }

      UDPLink _udpLink = (UDPLink) links.get(_linkId);
      // Now send a similar request back so that connection is created on other
      // side...

      byte[] magicBytes = magic.getBytes();
      byte[] udpConnRequest = new byte[1 + magicBytes.length];
      udpConnRequest[0] = cgl.narada.protocol.Protocol.CONN_REQ;

      System.arraycopy(magicBytes, 0, udpConnRequest, 1, magicBytes.length);

      System.out.println("Sending P2P_CONN_REQ Response...");
      try {
         _udpLink.sendData(udpConnRequest);
      } catch (TransportException e) {
         log.error("", e);
      }

      // Finally put this in the magic table
      // This implies that the destination's conn req successfully reached
      // here... so any further requests for connection establishment MUST stop
      magicTable.put(magic, _udpLink);
   }

   public void handleP2PConnectionResponse(byte[] data) {
      pd = P2PConnectionResponse.createObjectFromBytes(data);
      synchronized (waitForPeerDetails) {
         waitForPeerDetails.notify();
      }
   }

   public void handleRegisterPeerResponse(byte[] data) {

      // RegistrationResponse resp = RegistrationResponse
      // .createObjectFromBytes(data);
      //
      // log.debug("Register Peer Response ! MY Public ADDRESS {"
      // + resp.getPeerPublicAddress() + "}");
   }

   /**
    * This handles the P2P connection request that gets forwarded by the Relay
    * server. This essentially helps to punch a hole in this peer's NAT.
    * 
    * @param data
    */
   public void handleP2PConnectionRequest(byte[] data) {
      // P2P connection request recieved... Try sending Protocol.CONN_REQ to the
      // destined peer

      P2PConnectionSetupRequest setupRequest = P2PConnectionSetupRequest
               .createObjectFromBytes(data);

      if (setupRequest != null) {

         InetSocketAddress addr = (InetSocketAddress) setupRequest
                  .getRequestingPeersPublicAddress();

         String _hostName = addr.getHostName(); // linkProperties.getProperty("hostname");
         String _portString = "" + addr.getPort(); // linkProperties.getProperty("portnum");

         if (_hostName == null || _portString == null) {
            log.error("Unknown Host name / port !");
            return;
         }

         int _portNum = Integer.parseInt(_portString);
         _hostName = addr.getAddress().getCanonicalHostName();

         String _key = "udp://" + _hostName + ":" + _portNum;

         UDPLink _link = null;

         System.out.println("Trying: " + _key);

         if (links.containsKey(_key)) {
            // probably a duplicate...
            _link = (UDPLink) links.get(_key);
         } else {
            try {
               _link = new UDPLink(udpListenerSocket, addr.getAddress(),
                        _portNum, this);
               _link.setLinkId(_key);
            } catch (TransportException e) {
               log.error("", e);
               return;
            }
         }

         String magic = setupRequest.getMagic();

         /** Force the creation of a link on the other side too */
         byte[] magicBytes = magic.getBytes();
         byte[] udpConnRequest = new byte[1 + magicBytes.length];
         udpConnRequest[0] = cgl.narada.protocol.Protocol.CONN_REQ;

         System.arraycopy(magicBytes, 0, udpConnRequest, 1, magicBytes.length);

         // Create a separate Thread to do this so that incoming datagram
         // packets are not stopped from being processed
         (new ConnectorThread(_link, magic, udpConnRequest)).start();

         // if (linkEstablished(magic)) {
         // links.put(_key, magicTable.get(magic));
         // }
      }
   }

   private class ConnectorThread extends Thread {

      private UDPLink _link;
      private byte[] udpConnRequest;
      private String magic;

      /**
       * @param _link
       * @param magic
       * @param request
       */
      public ConnectorThread(UDPLink _link, String magic, byte[] request) {

         this._link = _link;
         this.magic = magic;
         udpConnRequest = request;
      }

      public void run() {
         // Try 5 times with 1 sec interval... OR until a link with our
         // current
         // magic is established...
         for (int z = 0; z < 5; z++) {
            if (linkEstablished(magic)) break;
            try {
               System.out.println("Trying {" + _link.getLinkId() + "}: "
                        + (z + 1) + " times");
               _link.sendData(udpConnRequest);
            } catch (TransportException e1) {
            }
            try {
               Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
         }
      }
   }

   public static void main(String[] args) {
      if (args.length != 4) {
         System.out
                  .println("Usage: java cgl.narada.transport.udp."
                           + "UP2PLinkFactory <udpListenerPort> <peerid> <relayserverhost> <relayserverport>");
         System.exit(0);
      }
      UP2PLinkFactory factory = new UP2PLinkFactory();

      Properties props = new Properties();
      props.put("UDPListenerPort", args[0]);
      props.put("PeerID", args[1]);
      props.put("RelayServerHost", args[2]);
      props.put("RelayServerPort", args[3]);
      factory.setLinkFactoryProperties(props);

      BufferedReader r = new BufferedReader(new InputStreamReader(System.in));
      String cmd;
      StringTokenizer tok;

      Hashtable links = new Hashtable();

      try {
         factory.loadLinkFactoryServices();
         System.out.println("\nType h for usage options");
         while (true) {
            cmd = r.readLine();

            if (cmd == null) {
               factory.dispose();
               break;
            }

            if (cmd.startsWith("h")) {
               System.out.println("cr <host> <port> \t \t \t Creates a link");
               System.out.println("cl <link-id> \t \t \t Closes a link");
               continue;
            }

            if (cmd.startsWith("cr")) {
               tok = new StringTokenizer(cmd, " ");
               if (tok.countTokens() != 3) {
                  System.out
                           .println("cr <host> <port> \t \t \t Creates a link");
                  continue;
               }
               Properties _props = new Properties();
               tok.nextToken();
               _props.put("hostname", tok.nextToken());
               _props.put("portnum", tok.nextToken());
               Link _link = factory.createLink(_props);
               links.put(_link.getLinkId(), _link);
               System.out.println("Created link with id " + _link.getLinkId());
            }
            /** if (creation of link */

            if (cmd.startsWith("cl")) {
               tok = new StringTokenizer(cmd, " ");
               if (tok.countTokens() != 2) {
                  System.out.println("cl <link-id> \t \t \t Closes a link");
                  continue;
               }
               tok.nextToken();
               String _linkId = tok.nextToken();
               if (links.containsKey(_linkId)) {
                  Link _link = (Link) links.remove(_linkId);
                  _link.closeLink();
               } else {
                  System.out.println("Specified link Id is not correct!");
               }
            } /* if (closing of a specified link ) */

            if (cmd.equalsIgnoreCase("q")) {
               factory.dispose();
               break;
            }
         }/* end while(true) */
      } catch (IOException ioe) {
         ioe.printStackTrace();
      } catch (TransportException transEx) {
         System.out.println(transEx);
      }/* end try-catch */
      System.out.println("UP2PLinkFactory: Exiting program ... end main()");
   }/* end-main */

}
